
// opengl
#include <gl/glew.h>
#pragma comment (lib, "glew32.lib")
#pragma comment (lib, "opengl32.lib")

// context
#define GLFW_INCLUDE_GL3
#define GLFW_NO_GLU
#define GLFW_DLL
#include <gl\glfw.h>
#pragma comment (lib, "glfwdll.lib")

// other
#include <iostream>
#include <functional>
#include <ctime>
using namespace std;

// bullet
#include "bullet\btBulletDynamicsCommon.h"
#pragma comment (lib, "../lib/BulletCollision.lib")
#pragma comment (lib, "../lib/BulletDynamics.lib")
#pragma comment (lib, "../lib/LinearMath.lib")

// my
#include "Application.h"
#include "Functor.h"
#include "SceneGraph.h"
#include "Mesh.h"
#include "Helpers.h"
#include "glm\glm.hpp"
#include "glm\ext.hpp"
using namespace glm;

bool pressed = false;
int touch_x = UNDEF, 
	touch_y = UNDEF;
int keyW = UNDEF,
	keyA = UNDEF,
	keyS = UNDEF,
	keyD = UNDEF;

int frames = 0;
char title[10];

Application& mainApp = Application::instance();
SceneGraph& mainSceneGraph = SceneGraph::instance();
Camera* cam = nullptr;
CameraController* camCtrl = nullptr;
Mesh* sharedMesh = nullptr;

btDiscreteDynamicsWorld* dynamicsWorld = nullptr;

void setupGraphics()
{
// fbo node
	FrameBufferNode* fbNode = new FrameBufferNode(800, 600);
	mainSceneGraph.addChild(fbNode);

// texture shader
	auto bindf = [](GLuint shaderProgram) {
		glBindAttribLocation(shaderProgram, ATTRIB_POSITION, "position");
		glBindAttribLocation(shaderProgram, ATTRIB_TEXCOORD0,"texcoord");
	};
	Shader* sh = new Shader("texShader", bindf);
	auto shn = new ShaderNode(sh);
	mainSceneGraph.addChild(shn);

// texture node
	TempNode* tempNode = new TempNode();
	shn->addChild(tempNode);

// projection node
	mat4 projMatrix = perspective(45.0f, 800.0f / 600.0f, 0.1f, 500.0f);
	auto projectionNode = new ProjectionNode(projMatrix);
	fbNode->addChild(projectionNode);

// view node
	cam = new Camera();
	cam->translate(vec3(0.0f, 50.0f, 300.0f));
	auto viewNode = new ViewNode(cam);
	projectionNode->addChild(viewNode);

// shader node
	auto bindFunction = [](GLuint shaderProgram) {
		glBindAttribLocation(shaderProgram, ATTRIB_POSITION, "position");
		glBindAttribLocation(shaderProgram, ATTRIB_COLOR,    "color");
	};
	Shader* shader = new Shader("shader", bindFunction);
	auto shaderNode = new ShaderNode(shader);
	viewNode->addChild(shaderNode);

// bunny shader node
	auto bf = [] (GLuint shaderProgram) {
		glBindAttribLocation(shaderProgram, ATTRIB_POSITION, "position");
		glBindAttribLocation(shaderProgram, ATTRIB_NORMAL,   "normal");
		glBindAttribLocation(shaderProgram, ATTRIB_COLOR,    "color");
	};
	Shader* bunnyShader = new Shader("bunnyShader", bf);
	ShaderNode* bunnyShaderNode = new ShaderNode(bunnyShader);
	viewNode->addChild(bunnyShaderNode);

// XYZ
	Mesh* xyzMesh = helper::mesh::getAxisMesh();

	auto meshNode = new MeshNode(GL_LINES);
	meshNode->setMesh(xyzMesh);
	shaderNode->addChild(meshNode);

// bunny
	Mesh* bunnyMesh = helper::mesh::getMeshFromFile("bunny", ColorRGBA(0.2f, 0.4f, 0.2f, 1.0f));
	bunnyMesh->_position.z += 100;
	bunnyMesh->_position.x -= 50;

	meshNode = new MeshNode(GL_TRIANGLES);
	meshNode->setMesh(bunnyMesh);
	bunnyShaderNode->addChild(meshNode);
	sharedMesh = bunnyMesh;

// teapot mesh
	/*
	int steps = 1;
	for (int i = 0; i < steps; i++) {
		for (int j = 0; j < steps; j++) {
			for (int k = 0; k < steps; k++) {
				Mesh* teapotMesh = helper::mesh::getMeshFromFile("teapot", ColorRGBA(0.2f, 0.4f, 0.2f, 1.0f));
				teapotMesh->_position.x -= i * 20;
				teapotMesh->_position.y -= j * 20;
				teapotMesh->_position.z -= k * 20;

				meshNode = new MeshNode(GL_TRIANGLES);
				meshNode->setMesh(teapotMesh);
				bunnyShaderNode->addChild(meshNode);
			}
		}
	}*/
	

// camera controller
	camCtrl = new CameraController(cam);
	mainApp.touchDownDelegate.addListener(camCtrl, &CameraController::onTouchDown);
	mainApp.touchMoveDelegate.addListener(camCtrl, &CameraController::onTouchMove);
	mainApp.touchUpDelegate.addListener(camCtrl, &CameraController::onTouchUp);
}

void setupPhysics()
{
// setup dynamics world
	btDefaultCollisionConfiguration * collisionConfiguration = new btDefaultCollisionConfiguration ();
	btCollisionDispatcher * dispatcher = new btCollisionDispatcher ( collisionConfiguration );
	btBroadphaseInterface * overlappingPairCache = new btDbvtBroadphase ();
	btSequentialImpulseConstraintSolver * solver = new btSequentialImpulseConstraintSolver () ;
	dynamicsWorld = new btDiscreteDynamicsWorld ( dispatcher , overlappingPairCache , solver , collisionConfiguration );
	dynamicsWorld -> setGravity ( btVector3 (0 , -10 ,0));

// setup rigid body
	///create a few basic rigid bodies
	btCollisionShape* groundShape = new btBoxShape(btVector3(btScalar(50.),btScalar(50.),btScalar(50.)));

	//keep track of the shapes, we release memory at exit.
	//make sure to re-use collision shapes among rigid bodies whenever possible!
	btAlignedObjectArray<btCollisionShape*> collisionShapes;

	collisionShapes.push_back(groundShape);

	btTransform groundTransform;
	groundTransform.setIdentity();
	groundTransform.setOrigin(btVector3(0,-56,0));

	{
		btScalar mass(0.);

		//rigidbody is dynamic if and only if mass is non zero, otherwise static
		bool isDynamic = (mass != 0.f);

		btVector3 localInertia(0,0,0);
		if (isDynamic)
			groundShape->calculateLocalInertia(mass,localInertia);

		//using motionstate is recommended, it provides interpolation capabilities, and only synchronizes 'active' objects
		btDefaultMotionState* myMotionState = new btDefaultMotionState(groundTransform);
		btRigidBody::btRigidBodyConstructionInfo rbInfo(mass,myMotionState,groundShape,localInertia);
		btRigidBody* body = new btRigidBody(rbInfo);

		//add the body to the dynamics world
		dynamicsWorld->addRigidBody(body);
	}


	{
		//create a dynamic rigidbody

		//btCollisionShape* colShape = new btBoxShape(btVector3(1,1,1));
		btCollisionShape* colShape = new btSphereShape(btScalar(1.));
		collisionShapes.push_back(colShape);

		/// Create Dynamic Objects
		btTransform startTransform;
		startTransform.setIdentity();

		btScalar	mass(1.f);

		//rigidbody is dynamic if and only if mass is non zero, otherwise static
		bool isDynamic = (mass != 0.f);

		btVector3 localInertia(0,0,0);
		if (isDynamic)
			colShape->calculateLocalInertia(mass,localInertia);

		startTransform.setOrigin(btVector3(2,50,0));
		
		//using motionstate is recommended, it provides interpolation capabilities, and only synchronizes 'active' objects
		btDefaultMotionState* myMotionState = new btDefaultMotionState(startTransform);
		btRigidBody::btRigidBodyConstructionInfo rbInfo(mass,myMotionState,colShape,localInertia);
		btRigidBody* body = new btRigidBody(rbInfo);

		dynamicsWorld->addRigidBody(body);
	}




}

void setup()
{
	setupGraphics();
	//setupPhysics();
}

bool GLWindowInit()
{
	//mainApp.setupDelegate.addListener(setup);
	//mainApp.drawDelegate.addListener(&mainSceneGraph, &SceneGraph::render);
	mainSceneGraph.init();
	setup();

	return true;
}

// 
void GLWindowRender()
{
	mainApp.draw();
	mainSceneGraph.render(nullptr);
}

void updatePhysics()
{
	dynamicsWorld->stepSimulation(1.f/60.f,10);
		
	//print positions of all objects
	btCollisionObject* obj = dynamicsWorld->getCollisionObjectArray()[1];
	btRigidBody* body = btRigidBody::upcast(obj);
	if (body && body->getMotionState())
	{
		btTransform trans;
		body->getMotionState()->getWorldTransform(trans);
		
		sharedMesh->_position = vec3(trans.getOrigin().getX(), trans.getOrigin().getY(), trans.getOrigin().getZ());
	}
}

// 
void GLWindowUpdate()
{
	float moveMult = 2.0;

	if (keyW != UNDEF) {
		cam->translate(vec3(0.0, 0.0, -moveMult));
	}
	if (keyA != UNDEF) {
		cam->translate(vec3(-moveMult, 0.0, 0.0));
	}
	if (keyS != UNDEF) {
		cam->translate(vec3(0.0, 0.0, moveMult));
	}
	if (keyD != UNDEF) {
		cam->translate(vec3(moveMult, 0.0, 0.0));
	}

	mainApp.update();
	cam->update();
	//updatePhysics();
}

void setFrameRateOnTitle()
{
	frames += 1;

	if (frames >= 20)
	{
		sprintf_s(title, "%f", frames / glfwGetTime());
		glfwSetWindowTitle(title);
		glfwSetTime(0);
		frames = 0;
	}
};

void GLFWCALL keyboardCallback(int key, int action)
{
	switch (key)
	{
	case 87: // w
		keyW = action == GLFW_RELEASE ? UNDEF : key;

		break;
	case 65: // a
		keyA = action == GLFW_RELEASE ? UNDEF : key;

		break;
	case 83: // s
		keyS = action == GLFW_RELEASE ? UNDEF : key;

		break;
	case 68: // d
		keyD = action == GLFW_RELEASE ? UNDEF : key;

		break;

	default:
		break;
	}
	
};
void GLFWCALL mousePosCallback( int x, int y )
{
	touch_x = x;
	touch_y = y;

	if (pressed)
		mainApp.onTouchMove((float)x, (float)y);
}
void GLFWCALL mouseButtonCallback(int button, int action)
{
	switch(button)
	{
	case GLFW_MOUSE_BUTTON_LEFT:
		if (action == GLFW_PRESS)
		{
			// left mb pressed
			pressed = true;

			mainApp.onTouchDown((float)touch_x, (float)touch_y);
		}
		else
		{
			// left mb released
			pressed = false;
			
			mainApp.onTouchUp((float)touch_x, (float)touch_y);
		}
		break;

	case GLFW_MOUSE_BUTTON_RIGHT:	
		break;

	case GLFW_MOUSE_BUTTON_MIDDLE:	
		break;
	}
}

int main(int argc, char* argv[])
{
	glfwInit();
	glfwOpenWindow(800, 600, 0, 0, 0, 0, 0, 0, GLFW_WINDOW);

	glewInit();
	GLWindowInit();

	// callbacks
	glfwSetKeyCallback((GLFWkeyfun)(keyboardCallback));
	glfwSetMousePosCallback((GLFWmouseposfun)(mousePosCallback));
	glfwSetMouseButtonCallback((GLFWmousebuttonfun)(mouseButtonCallback));

	// loop
	bool cont = true;
	while (cont)
	{
		// rendering 
		GLWindowUpdate();
		GLWindowRender();

		glfwSwapBuffers();

		// other ops 
		setFrameRateOnTitle();
		//glfwSetMousePos(400, 300);
		
		GL_CHECK_ERROR;

		cont = !glfwGetKey(GLFW_KEY_ESC);
	}

	glfwCloseWindow();
	glfwTerminate();

	return 0;
}